""" sbak/entity/base.py - Implements the basic entity types.
    
    Entities in this module are intended to be used as bases for more
    complex entities classes rather than for creating instances of
    them directly.
"""

import operator

import pygame
from pygame import Rect

from sbak.vector import Vector
from sbak import (
    ID_ANY,
    draw,
    group as group_,
    resman, # Needed to make Entity a Representable type
    image,
    event
)

from sbak.resman import Representable
from sbak.event import Responder, CE_UPDATE, CONTROLEVENT
from sbak.entity import register

__all__ = 'Entity', 'EntityM', 'Actor'

class EntityM(Responder.__metaclass__, Representable.__metaclass__):
    """ Metaclass for the Entity class.
    
        Combines the functionality of the metaclasses for
        `event.Responder` and `resman.Representable`.
    """
    def __init__(cls, name, bases, dict):
        super(EntityM, cls).__init__(name, bases, dict)
    

class Entity(Responder, group_.DynamicSprite, resman.Representable):
    """ Base for physical objects (entities).
    
        An "entity" is a physical object capable of existing in a
        world at a particular position and appearing as a certain
        image whenever a camera displays the world.
        
        Entity is derived from group.DynamicSprite. This means it can
        be added to ordinary pygame.sprite.Group objects by reference,
        but it can also be added to group.DynamicGroup objects by
        group ID.
        
        Although the Entity class is not actually considered a type of
        resource, it is a sub-class of resman.Representable. This makes
        it possible for entities to be represented as parts of worlds.
        Because of this, entity subclasses support the `tag` class
        attribute and the _rep_ and _unrep_ prefixes. (See sbak.resman
        for more details.)
        
        The default tag for Entity is 'entity'.
        
        ~~~ Attributes ~~~
                    
        hidden:     If true, the entity will not be drawn when the
                    containing world is drawn.
                    
        solid:      If true, the entity partakes in collisions. This
                    means that whenever a collision check is performed
                    by something other than the entity, the entity will
                    be included in the list of objects with which the
                    other object could potentially be colliding. If
                    false, the entity becomes "ethereal;" that is,
                    other things will not (normally) collide with it.
        
        layer:      Controls the order in which entities are drawn in a
                    world. Entities are drawn in order of lowest to
                    highest layer. This MUST be an integer or else
                    errors will occur during processing.
    """
    
    __metaclass__ = EntityM
    
    tag = 'entity'
    
    # ~~~ Internal handling methods ~~~
    
    def __init__( self,
                  id = None,
                  primary = False,
                  world = None,
                  pos = None,
                  image = None,
                  layer = 0,
                  hidden = False,
                  groups = None,
                  x = None, y = None ):
        """ Initializes an object conforming to the Entity structure.
            
            FIXME: id:     Unique entity ID. Should be a string. Pass None to
                    not give the entity an ID, or pass the constant
                    ID_ANY to cause a new, unused ID to be generated
                    using `sbak.entity.generate_id()`. The `tag`
                    argument will be the `tag` attribute of the
                    Entity class. (Hint: overwriting the `tag`
                    class attribute in sublclasses will change the
                    `tag` value passed to generate_id() when subclass
                    instances are created.)
            
            world:  The world to place the entity into. This can be a
                    World object, or if the entity is part of a game,
                    a file name.
                    
            pos:    Pass a vector or sequence of two numbers. This will
                    be the position of the entity relative to the
                    top-left corner of the world in which it placed.
                    
            image:  Image used to display the entity in worlds. May be
                    a Surface, Image or Image subclass object, or if
                    the entity is part of a Game, a file name.
                    
            layer:  Controls drawing order. Entities are drawn in order
                    of lowest to highest layer, such that the lowest-
                    layered entity is drawn first and the highest is
                    drawn last. Defaults to 0.
                    
            hidden: Whether or not to hide the entity. If this is True
                    the entity will not be drawn even if it has an
                    image.
                    
            groups: An initial set of groups to add the entity to. Pass
                    a sequence of group objects and/or group IDs. If
                    an ID is used then sbak.group.get will be used to
                    get the group. NOTE: The 'entity' DynamicGroup ID
                    is always added to this sequence--do not specify it
                    yourself.
            
            x, y:   Instead of passing the position as a vector or 2-
                    number sequence, you can pass individual
                    components.
        """
        
        # Initialize event responder components
        Responder.__init__(self)
        self.id = id
        
        # Set up basic event handler(s)
        self.hook(self.handle_update, CONTROLEVENT, control=CE_UPDATE)

        # Initialize dynamic-sprite components, add to the groups.
        groups = set(groups if groups is not None else [])
        groups.add('entity')
        group_.DynamicSprite.__init__(self, *groups)
        
        # Set default values for internals
        self._pos = Vector()
        self._image = None # Drawable to represent entity
        self.layer = 0 # Determines draw order
        self._on_kill = [] # Handlers for when the entity dies.
        self.solid = False
        
        # Get the values from the caller
        self.set_parent(world)
        if pos is not None:
            self.pos = pos
        if x is not None:
            self.pos.x = x
        if y is not None:
            self.pos.y = y
        if image is not None:
            self.image = image
        self.layer = int(layer)
        self.hidden = bool(hidden)
    
    __nonzero__ = group_.DynamicSprite.alive
    
    # ~~~ Representable attributes ~~~
    # Read the docs at sbak.resman.Representable to understand how these work.
    
    _rep_world = lambda self: self.parent
    _rep_layer = lambda self: int(self.layer)
    _rep_hidden = lambda self: bool(self.hidden)
    _rep_groups = lambda self: [g.id for g in self.groups() if hasattr(g, 'id')]
    
    # ~~~ Other methods ~~~
    
    def handle_update(self, e=None):
        """ Basic update control event handler for entities.
        
            The basic entity updater doesn't do anything except update
            the entity's image if it has one.
            
            If the entity is in a world and not disabled, this method
            will be called for every entity in the world each time
            the world is updated.
            
            The method accepts an optional Event or Hook object (which
            it does not actually use) so that it can be used as an
            event handler for a hook.
            
            This handler is hooked on creation of the entity with the
            default priority.
        """
        # Only update the image if there actually is an image and it has
        # an update method (meaning it is not a Surface)
        if self.image is not None and hasattr(self.image, 'update'):
            self.image.update()
    
    def draw(self, dest=None, offset=(0,0), region=None):
        """ Draws the entity's image to a surface.
        
            dest:   Pass the surface onto which the entity should be
                    drawn, or None to draw directly to the screen.
                    
            offset: Pass a sequence of 2 numbers. This offset will be
                    added to the entity's position to get the position
                    of the surface at which the entity will be drawn.
                    (Note: The final coordinates will be converted to
                    integers.)
                    
            region: Pass None, a pygame Rect object, or a compatible
                    object. If the argument is not None, the entity's
                    sprite will only be drawn if the sprite's area in
                    relation to the entity intersects with the area
                    given and it will be drawn with an additional
                    offset of region.topleft.
            
            Worlds will not draw entities if their `hidden` attribute
            is true.
        """        
        # Don't draw if there is no image
        if self._image is not None:
            selfPos = self._pos
            selfImage = self._image
            
            # Don't use region if it is None
            if region is not None:
                
                # Don't do anything else if sprite is not touching area
                if not selfImage.get_rect(topleft=selfPos).colliderect(region):
                    return
                
                pos = map(sum, zip(selfPos, offset, [-v for v in region[0:2]]))
            
            else:
                pos = map(sum, zip(selfPos, offset))
            
            draw.image(selfImage,dest,pos)
    
    #def _remove_hook(self, hook):
    #    self._hooks.remove(hook)
    
    def kill(self):
        """ Kills the entity.
            
            Killing an entity causes the following to occur in order:
            
            1. Execution of all on_kill handlers,
            2. Emptying of on_kill handler list,
            3. Removal of entity from its world,
            4. Removal of entity from all groups.
        """
        [h(self) for h in self.on_kill]
        self._on_kill[:] = []
        Responder.kill(self)
        group_.DynamicSprite.kill(self)
    
    def _set_on_kill(self,val):
        raise AttributeError("Read-only attribute. Use "
                             "on_kill.append(h) to add handlers.")
    on_kill = property(
        fget = lambda self: self._on_kill,
        fset = _set_on_kill,
        doc =
        """ Kill pseudo-event handlers.
        
            This will be a list of callables that will be called when
            the entity is killed, after which the list is emptied.
            
            This property is read-only, but the list it refers to is
            not, so handlers may be added like this:
            
            >>> entity.on_kill.append(handler)
            >>> entity.on_kill.append(handler2)
            
            After that, `handler(entity)` will be called followed by
            `handler2(entity)` when the entity is killed.
        """)
    
    def set_image(self,image):
        if isinstance(image, basestring):
            image = self.get_root().resources.get(image, 'anyimage')
        self._image = image
    image = property(
        fget = operator.attrgetter('_image'),
        fset = set_image,
        doc =
        """ The drawable object used to draw the entity.
            
            You may set this to the ID of a surface or image resource (or
            compatible). However, this will set the value of the property to
            the actual resource object, not the ID string. An exception will
            be raised if the resource is unavailable.
            
            NOTE: Even after implicitly loading an image by passing a
            file name, this property will contain an image rather
            than the name of the file. 
        """)
    _rep_image = lambda self, m, n: m.res_to_json(self.image, n, True)
    _unrep_images = lambda self, v, m: \
        m.get(m, 'anyimage') if isinstance(v, basestring) else image.from_json(v, m)
    
    def set_pos(self, pos):
        self._pos = Vector(pos)
    pos = property(
        fget = lambda self: self._pos,
        fset = set_pos,
        doc = " Position of the entity in the world as a Vector. "
    )
    _rep_pos = lambda self: self.pos
    
    def set_x(self, val):
        self._pos.x = float(val)
    x = property(
        fget = lambda self: self._pos[0],
        fset = set_x,
        doc = " X component of position vector. ")
    
    def set_y(self, val):
        self._pos.y = float(val)
    y = property(
        fget = lambda self: self._pos[1],
        fset = set_y,
        doc = " Y component of position vector. ")

    def get_universe(self):
        """ Returns the universe that the entity is in.
        
            If the entity is not in a universe, this returns None.
        """
        try:
            return self.parent.get_universe()
        except AttributeError:
            return None

class Actor(Entity):
    """ An entity that...acts.
    
        This type of entity has the capabilies of movement, gravity,
        and friction in addition to the normal capabilities of an
        entity. It also has additional collision detection methods
        that make it easy to check for collisions with other entities
        in the same world.
        
        TODO: Remove x/y/aspeed in favor of `velocity` Vector
        TODO: Remove x/y/agravity in favor of `gravity` Vector
    """
    
    tag = 'actor'
    
    def __init__( self,
                  id = None,
                  bbox = None,
                  solid = False,
                  aspeed = None,
                  agravity = None,
                  afriction = None,
                  w = None,
                  width = None,
                  h = None,
                  height = None,
                  size = None,
                  top = None,
                  left = None,
                  bottom = None,
                  right = None,
                  topleft = None,
                  topright = None,
                  bottomleft = None,
                  bottomright = None,
                  xspeed = None,
                  yspeed = None,
                  xgravity = None,
                  ygravity = None,
                  xfriction = None,
                  yfriction = None,
                  **kwargs ):
        """ Actor accepts any of the same arguments that the Entity
            constructor does (in the form of keyword arguments), and
            additionally supports the following:
            
            bbox:       A rectangular area relative to the Actor's
                        position that the Actor occupies. Defaults
                        to (0,0,0,0), meaning that no space is
                        occupied.
            
            solid:      A flag that indicates whether or not the
                        actor partakes in collisions. If False
                        (default), the actor will be overlooked when
                        searching for collisions (like an ordinary
                        entity). If True, the actor is asked if it
                        is colliding with bounding boxes whenever
                        a collision check is being performed.
            
            aspeed:     The initial speed of the entity as an algebraic
                        (x,y) vector. The default speed is 0,0 (no
                        motion).
            
            agravity:   The rate of acceleration as an algebraic
                        (x,y) vector. The default is 0,0 (speed
                        remains constant)
            
            afriction:  Friction (rate at which speed approaches 0) as
                        an algebraic (x,y) vector. The default is
                        0,0 (no friction).
            
            Individual components of the above can be given using
            the following arguments:
            
            w
            width
            h
            height
            size
            top
            left
            bottom
            right
            topleft
            topright
            bottomleft
            bottomright
            xspeed
            yspeed
            xgravity
            ygravity
            xfriction
            yfriction
        """
        
        # Pass unhandled attributes to the base constructor
        super(Actor,self).__init__( id=id, **kwargs )
        
        # Set default internals
        self._bbox = Rect(0,0,0,0)
        self._aspeed = [0,0]
        self._agravity = [0,0]
        self._afriction = [0,0]
        
        # Explicitly interpret each of the arguments passed to the
        # constructor. This guaruntees that only the built-in
        # properties of the Actor class are used and helps prevents
        # bugs in user code.
        
        if w is not None:
            self.w = w
        if width is not None:
            self.width = width
        if h is not None:
            self.h = h
        if height is not None:
            self.height = height
        if size is not None:
            self.size = size
        if top is not None:
            self.top = top
        if left is not None:
            self.left = left
        if right is not None:
            self.right = right
        if topright is not None:
            self.topright = topright
        if topleft is not None:
            self.topleft = topleft
        if bottomright is not None:
            self.bottomright = bottomright
        if xpseed is not None:
            self.xspeed = xspeed
        if yspeed is not None:
            self.yspeed = yspeed
        if xgravity is not None:
            self.xgravity = xgravity
        if ygravity is not None:
            self.ygravity = ygravity
        if xfriction is not None:
            self.xfriction = xfriction
        if yfriction is not None:
            self.yfriction = yfriction

    def set_size(self,size):
        oldX,oldY,oldW,oldH = self._bbox
        newW,newH = size
        self._bbox[:4] = oldX*(oldW/newW), oldY*(oldW/newH), newW, newH
    size = property(
        fget = lambda self: self._bbox.size,
        fset = set_size,
        doc =
        """ Size as (int,int) pair.
        
            Changing the size of the entity through this and related
            properties and methods (other than bbox) will cause the
            top-left corner of the bbox to be scaled so that it keeps
            its ratio to the entity's position. (If you don't
            understand what this means, just try changing the top-left
            corner of the bbox to something other than (0,0), then
            change the size and note how it affects the bbox's top-left
            corner.)
            
            If you do not want the behavior above, use Entity.bbox.size
            rather than Entity.size.
        """)
    
    def set_w(self,w):
        bbox = self._bbox
        bbox.x *= bbox.width/w
        bbox.width = w
    width = w = property(
        fget = lambda self: self._bbox.width,
        fset = set_w,
        doc = " Width as int. See note at `size`.")
    
    def set_h(self,h):
        bbox = self._bbox
        bbox.y *= bbox.height/h
        bbox.height = h
    height = h = property(
        fget = lambda self: self._bbox.height,
        fset = set_h,
        doc = " Height as int. See note at `size`. ")

    # TODO: Move these reppers to next to their appropriate properties
    _rep_solid = lambda self: self.solid
    _rep_aspeed = lambda self: self.aspeed
    _rep_agravity = lambda self: self.agravity
    _rep_afriction = lambda self: self.afriction

    def handle_update(self,e=None):
        """ Updates an actor.
        
            In addition to the basic Entity behavior, this currently
            does the following:
            
            * Adds algebraic gravity vector to algebraic speed vector,
            * Adds/subtracts algebraic friction vector to/from speed
              vector (so speed heads towards 0)
            * Adds algebraic speed vector to algebraic coordidante (the
              entity's position).
            
            Subclasses should call this if they overwrite it and they
            motion to be handled.
        """
        super(Actor,self).handle_update(e)
        
        # Localize for less typing
        x, y = self._pos
        xs, ys = self._aspeed
        xg, yg = self._agravity
        xf, yf = self._afriction
        
        # Apply gravity
        xs += xg
        ys += yg
        
        # Apply friction
        if xs > 0:
            xs -= xf
            if xs < 0:
                xs = 0
        elif xs < 0:
            xs += xf
            if xs > 0:
                xs = 0
        if ys > 0:
            ys -= yf
            if ys < 0:
                ys = 0
        elif ys < 0:
            ys += yf
            if ys > 0:
                ys = 0
        
        # Apply speed
        x += xs
        y += ys
        
        # Save changes
        self._pos = [x,y]
        self._aspeed = [xs,ys]
        self._agravity = [xg,yg]
        self._afriction = [xf,yf]

    def set_bbox(self, bbox):
        self._bbox = Rect(bbox)
    bbox = property(
        fget = lambda self: self._bbox,
        fset = set_bbox,
        doc = " Bounding box as pygame rect. (See get_bbox for more info.) ")

    region = property(
        fget = lambda self: self._bbox.move(self._pos),
        doc = " Region occupied as pygame rect. Read-only. ")
    
    def set_left(self,L):
        self._pos[0] = L-self._bbox.left
    left = property(
        fget = lambda self: self._pos[0]+self._bbox.left,
        fset = set_left,
        doc = " Left edge of occupied region as float. ")
    
    def set_right(self,r):
        self._pos[0] = r-self._bbox.right
    right = property(
        fget = lambda self: self._pos[0]+self._bbox.right,
        fset = set_right,
        doc = " Right edge of occupied region as float. ")
    
    def set_top(self,t):
        self._pos[1] = t-self._bbox.top
    top = property(
        fget = lambda self: self._pos[1]+self._bbox.top,
        fset = set_top,
        doc = " Top edge of occupied region as float. ")
    
    def set_bottom(self,b):
        self._pos[1] = b-self._bbox.bottom
    bottom = property(
        fget = lambda self: self._pos[1]+self._bbox.bottom,
        fset = set_bottom,
        doc = " Bottom edge of occupied region as float. ")
    
    def set_topleft(self,p):
        self._pos = [float(i)-j for i,j in zip(p, self._bbox.topleft)]
    topleft = property(
        fget = lambda self: map(sum, zip(self._pos, self._bbox.topleft)),
        fset = set_topleft,
        doc = " Top-left-most point occupied as pair of floats. ")
    
    def set_topright(self,p):
        self._pos = [float(i)-j for i,j in zip(p, self._bbox.topright)]
    topright = property(
        fget = lambda self: map(sum, zip(self._pos, self._bbox.topleft)),
        fset = set_topright,
        doc = " Top-right-most point occupied as pair of floats. ")
    
    def set_bottomleft(self,p):
        self._pos = [float(i)-j for i,j in zip(p, self._bbox.bottomleft)]
    bottomleft = property(
        fget = lambda self: map(sum, zip(self._pos, self._bbox.bottomleft)),
        fset = set_bottomleft,
        doc = " Bottom-left-most point occupied as pair of floats. ")
    
    def set_bottomright(self,p):
        self._pos = [float(i)-j for i,j in zip(p, self._bbox.bottomright)]
    bottomright = property(
        fget = lambda self: map(sum, zip(self._pos, self._bbox.bottomright)),
        fset = set_bottomright,
        doc = " Bottom-right-most point occupied as pair of floats. ")
    
    def collide(self, rect, offset=(0,0)):
        """ Asks the entity if it is colliding with a rectangle.
        
            ~~~ ALIASES ~~~
        
            ent.collide(rect, offset=(0,0)) -> bool
            ent.colliderect(rect, offset=(0,0)) -> bool
        
            ~~~ ARGUMENTS ~~~
        
            rect:   A rectangular region as Rect or Rect-like sequence.
            
            offset: Optional offset (x,y sequence) to temporarily apply
                    to own region during the check.
        
            ~~~ DESCRIPTION ~~~
        
            This is (probably) the simplest collision detection method
            available for an entity. Given a rectangular region, the
            entity compares its own region against the given rectangle.
            If the two rectangles overlap at all, the method returns
            True, otherwise it returns False.
            
            An optional offset may be passed to check if there would be
            a collision with the rectangle if the entity were moved by
            the offset instead of checking where the entity actually is
            at present. This is useful, for example, to see if an
            entity in motion* is about to collide with a rectangle.
            
            * Motion is not provided by Entity; use Actor for that.
        """
        return self.region.move(offset).colliderect(rect)
    
    colliderect = collide
    
    def collide_group(self, rect, group, offset=(0,0)):
        """ Like collide, but checks entity group membership.
        
            rect:   A rectangular region as Rect or Rect-like sequence.
            
            group:  A pygame.sprite.Group object, or compatible, or
                    a string. If the group is a string, the
                    group.get() function is used to translate it to
                    a group.
            
            offset: Optional offset (x,y sequence) to temporarily apply
                    to own region during the check.
            
            This method provides the same functionality as the
            collide method, but also optionally checks to see
            if the entity is a member of a particular group. If the
            entity is not a member, the method returns False even if
            the entity is colliding with the given rectangle.
            
            For the curious: This method is obviously somewhat
            pointless for 
        """
        if isinstance(group,basestring):
            group = group_.get(group)
        if self not in group:
            return False
        return self.collide(rect, offset)
    
    def collide_groups(self, rect, groups, offset=(0,0)):
        """ Like collide_group, but checks for multiple groups.
        
            rect:   A rectangular region as Rect or Rect-like sequence.
            
            groups: A sequence of pygame.sprite.Group objects, and/or
                    compatible objects, and/or strings. The check will
                    be false is the entity is not a member of all the
                    groups in the list.
            
            offset: Optional offset (x,y sequence) to temporarily apply
                    to own region during the check.
                    
            This method works the same way as the collide_group
            method, but allows for checking for membership with more
            than one group. If the entity is not a member of all the
            groups, the method returns false even if the entity's
            region is colliding with the rectangle.
        """
        for grp in groups:
            if isinstance(grp,basestring):
                grp = group.get(grp)
            if self not in grp:
                return False
        return self.region.move(offset).colliderect(rect)

    def set_aspeed(self, val):
        self._aspeed = map(float, val[:2])
    aspeed = property(
        fget = lambda self: tuple(self._aspeed),
        fset = set_aspeed,
        doc =
        """ The current speed as an algebraic vector (float,float tuple).
            
            This is the number of pixels the entity will move along the
            x and y axes when the update() method is called.
        """)
    
    def set_xspeed(self, val):
        self._aspeed[0] = float(val)
    xspeed = property(
        fget = lambda self: self._aspeed[0],
        fset = set_xspeed,
        doc =
        """ The current horizontal speed as float.
        
            This is the number of pixels added to the x position each
            time update() is called.
        """)
    
    def set_yspeed(self, val):
        self._aspeed[1] = float(val)
    yspeed = property(
        fget =lambda self: self._aspeed[1],
        fset = set_yspeed,
        doc =
        """ The current vertical speed as float.
        
            This is the number of pixels added to the y position each
            time update() is called.
        """)
    
    def set_agravity(self, val):
        self._agravity = map(float, val[:2])
    agravity = property(
        fget = lambda self: tuple(self._agravity),
        fset = set_agravity,
        doc =
        """ The current gravity as an algebraic (float,float) vector
            
            Thes are the values that are added to the algebraic x and y
            speeds when the update() method is called.
            
            An effective alias to (actor.xgravity, actor.ygravity).
        """)
    
    def set_xgravity(self, val):
        self._agravity[0] = float(val)
    xgravity = property(
        fget = lambda self: self._agravity[0],
        fset = set_xgravity,
        doc =
        """ The current horizontal gravity as float.
        
            This value is added to xspeed each update.
            
            An effective alias to actor.agravity[0].
        """)
    
    def set_ygravity(self, val):
        self._agravity[1] = float(val)
    ygravity = property(
        fget = lambda self: self._agravity[1],
        fset = set_ygravity,
        doc =
        """ The current vertical gravity as float.
        
            This value is added to yspeed each update.
            
            An effective alias to actor.agravity[1].
        """)
    
    def set_afriction(self,val):
        self._afriction = map(float, val[:2])
    afriction = property(
        fget = lambda self: tuple(self._afriction),
        fset = set_afriction,
        doc =
        """ Friction as algebraic vector.
        
            These are the values that are either added to or subtracted from
            the algebraic x and y speeds until they reach 0.
            
            An effective alias to (actor.xfriction, actor.yfriction).
        """)
    
    def set_xfriction(self,val):
        self._afriction[0] = float(val)
    xfriction = property(
        fget = lambda self: self._afriction[0],
        fset = set_xfriction,
        doc = " Horizontal friction. ")
    
    def set_yfriction(self,val):
        self._afriction[1] = float(val)
    yfriction = property(
        fget = lambda self: self._afriction[1],
        fset = set_yfriction,
        doc = " Vertical friction. ")
    
    def find_collider(self, offset=(0,0)):
        """ Returns the first entity that is colliding with self.
        
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
                    
            Returns an entity or None.
        """
        if self.parent is not None:
            return self.parent.find_collider(self.region.move(offset), self)
    
    def find_collider_group(self, group, offset=(0,0)):
        """ Like find_collider, but checks for entity group membership.
        
            group:  A group that the other entity must be a member of.
            
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
                    
            Returns an entity or None.
        """
        if self.parent is not None:
            return self.parent.find_collider_group(self.region.move(offset),
                                                  self, group)
    
    def find_collider_groups(self, groups, offset=(0,0)):
        """ Like find_collider_group, but checks for multiple groups.
        
            groups: A sequence of groups that the entity must be a
                    member of.
            
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
                    
            Returns an entity or None.
        """
        if self.parent is not None:
            return self.parent.find_collider_groups(self.region.move(offset),
                                                  self, groups)
    
    def find_colliders(self, offset=(0,0)):
        """ Returns the entities that are colliding with self.
        
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
                    
            Returns a list of 0 or more entities.
        """
        if self.parent is not None:
            return self.parent.find_colliders(self.region.move(offset), self)
        return []
    
    def find_colliders_group(self, group, offset=(0,0)):
        """ Like find_colliders, but checks for entity group membership.
        
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
            
            group:  A group that the other entity must be a member of.
                    
            Returns a list of 0 or more entities.
        """
        if self.parent is not None:
            return self.parent.find_colliders_group(self.region.move(offset),
                                                   self, group)
    
    def find_colliders_groups(self, groups, offset=(0,0)):
        """ A combination of find_colliders and find_collider_groups.
        
            groups: A sequence of groups that all other entities must
                    be members of.
        
            offset: Optional offset to apply to own region temporarily
                    while searching. Useful for fiding out if the actor
                    would collide with something if it were moved.
            
            Returns a sequence of 0 or more entities.
        """
        if self.parent is not None:
            return self.parent.find_colliders_groups(self.region.move(offset),
                                                  self, groups)
        return []
